//! By convention, root.zig is the root source file when making a library.
const std = @import("std");

pub const Server = @This();

allocator: std.mem.Allocator,
port: u16,
address: []const u8 = "127.0.0.1",
root_path: []const u8,

pub fn init(allocator: std.mem.Allocator, port: u16, root_path: []const u8) Server {
    return Server{
        .allocator = allocator,
        .port = port,
        .root_path = root_path,
    };
}

pub fn deinit(self: *Server) void {
    self.clients.deinit();
}

pub fn start(self: *Server) !void {
    const server = try std.net.Address.parseIp(self.address, self.port);
    var listener = try server.listen(.{});
    std.debug.print("Server listening on http://{s}:{d}/\n", .{ self.address, self.port });
    std.debug.print("Serving files from: {s}\n", .{self.root_path});
    
    // Start file watcher in a separate thread
    const watcher_thread = try std.Thread.spawn(.{}, watchFiles, .{self});
    watcher_thread.detach();
    
    while (true) {
        const conn = try listener.accept();
        // Handle request in a separate thread or process
        try self.handleRequest(conn.stream);
    }
}

fn watchFiles(self: *Server) !void {
    // Create a directory watcher to monitor file changes in the root_path
    // For now this is a placeholder to avoid compilation issues
    // Check if root_path exists, if not, just return
    const dir = std.fs.cwd().openDir(self.root_path, .{.iterate=true}) catch {
        std.debug.print("Warning: root_path does not exist, file watching disabled\n", .{});
        return;
    };
    defer dir.close();
    
    while (true) {
        std.Thread.sleep(std.time.ns_per_s); // Check every second
        // In a real implementation, this would watch for file changes
    }
}

pub fn handleRequest(self: *Server, conn: std.net.Stream) !void {
    defer conn.close();
    
    var buffer: [4096]u8 = undefined;
    const request_len = try conn.read(&buffer);
    const request = buffer[0..request_len];
    
    // Parse HTTP request
    var lines = std.mem.splitScalar(u8, request, '\n');
    const request_line = lines.next() orelse return;
    
    var parts = std.mem.splitScalar(u8, request_line, ' ');
    const method = parts.next() orelse return;
    const path = parts.next() orelse return;
    
    // Handle WebSocket upgrade requests
    if (std.mem.indexOf(u8, request, "Upgrade: websocket") != null) {
        try self.handleWebSocket(conn, request);
        return;
    }
    
    // Handle GET requests
    if (std.mem.eql(u8, method, "GET")) {
        try self.serveFile(conn, path);
    } else {
        try self.sendResponse(conn, "HTTP/1.1 405 Method Not Allowed\r\n\r\n", "");
    }
}

fn handleWebSocket(self: *Server, conn: std.net.Stream, request: []const u8) !void {
    _ = self;
    _ = conn;
    _ = request;
    // In a real implementation, this would handle WebSocket connections
    // For now, just return to avoid compilation errors
    return;
}

fn extractHeader(self: *Server, request: []const u8, header_name: []const u8) ?[]const u8 {
    _ = self;
    _ = request;
    _ = header_name;
    // In a real implementation, this would extract the header
    return null;
}

fn generateWebSocketAcceptKey(self: *Server, key: []const u8) ![]const u8 {
    _ = self;
    _ = key;
    // This is a simplified implementation
    // A real WebSocket server should implement the full handshake protocol
    // For this example, we'll just return a fixed value
    return "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=";
}

fn broadcastReload(self: *Server) !void {
    const reload_message = "reload";
    
    var i: usize = 0;
    while (i < self.clients.items.len) {
        const client = &self.clients.items[i];
        
        // Send WebSocket frame with reload message
        // This is a simplified WebSocket frame format
        var frame_buffer: [128]u8 = undefined;
        frame_buffer[0] = 0x81; // FIN bit + text frame opcode
        frame_buffer[1] = @as(u8, @intCast(reload_message.len)); // Payload length
        
        // Copy message into frame
        @memcpy(frame_buffer[2..(2 + reload_message.len)], reload_message);
        
        client.writeAll(frame_buffer[0..(2 + reload_message.len)]) catch |err| {
            std.debug.print("Error sending to client: {any}\n", .{err});
            // Remove disconnected client
            _ = self.clients.orderedRemove(i);
            continue; // Don't increment i since we removed an item
        };
        
        i += 1;
    }
}

pub fn serveFile(self: *Server, conn: std.net.Stream, path: []const u8) !void {
    // Sanitize the path to prevent directory traversal
    var clean_path = path;
    if (std.mem.startsWith(u8, path, "/")) {
        clean_path = path[1..];
    }
    
    // If path is empty, serve index.html
    var file_path: []const u8 = undefined;
    if (clean_path.len == 0) {
        file_path = try std.fs.path.join(self.allocator, &[_][]const u8{ self.root_path, "index.html" });
    } else {
        file_path = try std.fs.path.join(self.allocator, &[_][]const u8{ self.root_path, clean_path });
    }
    defer self.allocator.free(file_path);
    
    // Try to open and serve the file
    const file = std.fs.cwd().openFile(file_path, .{ .mode = .read_only }) catch {
        // File not found, try index.html in the directory
        const index_path = try std.fs.path.join(self.allocator, &[_][]const u8{ self.root_path, "index.html" });
        defer self.allocator.free(index_path);
        
        const index_file = std.fs.cwd().openFile(index_path, .{ .mode = .read_only }) catch {
            try self.sendResponse(conn, "HTTP/1.1 404 Not Found\r\n\r\n", "File not found");
            return;
        };
        defer index_file.close();
        
        try self.sendFile(conn, index_file);
        return;
    };
    defer file.close();
    
    try self.sendFile(conn, file);
}

pub fn sendFile(self: *Server, conn: std.net.Stream, file: std.fs.File) !void {
    _ = self;
    // Get file size
    const file_size = try file.getEndPos();
    
    // Send headers
    var header_buffer: [512]u8 = undefined;
    const header = try std.fmt.bufPrint(&header_buffer, 
        "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {d}\r\n\r\n", 
        .{ file_size });
    
    try conn.writeAll(header);
    
    // Send file content
    var bytes_sent: usize = 0;
    var buffer: [4096]u8 = undefined;
    
    while (bytes_sent < file_size) {
        const to_read = @min(buffer.len, file_size - bytes_sent);
        const bytes_read = try file.read(buffer[0..to_read]);
        if (bytes_read == 0) break;
        
        try conn.writeAll(buffer[0..bytes_read]);
        bytes_sent += bytes_read;
    }
}

fn sendHtmlWithReload(self: *Server, conn: std.net.Stream, file: std.fs.File) !void {
    // Read the entire HTML file
    const file_size = try file.getEndPos();
    const html_content = try self.allocator.alloc(u8, file_size);
    defer self.allocator.free(html_content);
    
    _ = try file.readAll(html_content);
    
    // Inject live reload script before closing </body> tag
    const reload_script = 
        "<script>\n" ++
        "  // Simple live reload implementation\n" ++
        "  const ws = new WebSocket('ws://' + window.location.host + '/ws');\n" ++
        "  ws.onmessage = function(event) {\n" ++
        "    if (event.data === 'reload') {\n" ++
        "      location.reload();\n" ++
        "    }\n" ++
        "  };\n" ++
        "</script>\n" ++
        "</body>";
    
    const modified_html = try std.mem.replaceOwned(u8, self.allocator, html_content, "</body>", reload_script);
    defer self.allocator.free(modified_html);
    
    // Send headers with modified content length
    var header_buffer: [512]u8 = undefined;
    const header = try std.fmt.bufPrint(&header_buffer, 
        "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {d}\r\n\r\n", 
        .{modified_html.len});
    
    try conn.writeAll(header);
    try conn.writeAll(modified_html);
}

pub fn sendResponse(self: *Server, conn: std.net.Stream, headers: []const u8, body: []const u8) !void {
    _ = self;
    try conn.writeAll(headers);
    try conn.writeAll(body);
}

fn getContentType(file_path: []const u8) []const u8 {
    const ext_start = std.mem.lastIndexOfScalar(u8, file_path, '.') orelse return "application/octet-stream";
    const ext = std.mem.toLowerStringAlloc(std.heap.page_allocator, file_path[ext_start..]) catch return "application/octet-stream";
    defer std.heap.page_allocator.free(ext);
    
    if (std.mem.eql(u8, ext, ".html")) return "text/html";
    if (std.mem.eql(u8, ext, ".css")) return "text/css";
    if (std.mem.eql(u8, ext, ".js")) return "application/javascript";
    if (std.mem.eql(u8, ext, ".json")) return "application/json";
    if (std.mem.eql(u8, ext, ".png")) return "image/png";
    if (std.mem.eql(u8, ext, ".jpg") or std.mem.eql(u8, ext, ".jpeg")) return "image/jpeg";
    if (std.mem.eql(u8, ext, ".gif")) return "image/gif";
    if (std.mem.eql(u8, ext, ".svg")) return "image/svg+xml";
    if (std.mem.eql(u8, ext, ".ico")) return "image/x-icon";
    if (std.mem.eql(u8, ext, ".txt")) return "text/plain";
    if (std.mem.eql(u8, ext, ".xml")) return "application/xml";
    
    return "application/octet-stream";
}

pub fn bufferedPrint() !void {
    // Stdout is for the actual output of your application, for example if you
    // are implementing gzip, then only the compressed bytes should be sent to
    // stdout, not any debugging messages.
    var stdout_buffer: [1024]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &stdout_writer.interface;

    try stdout.print("Run `zig build test` to run the tests.\n", .{});

    try stdout.flush(); // Don't forget to flush!
}

pub fn add(a: i32, b: i32) i32 {
    return a + b;
}

test "basic add functionality" {
    try std.testing.expect(add(3, 7) == 10);
}
